﻿using System;
using System.Collections.Generic;
using System.ComponentModel;
using System.Linq;
using System.Text.RegularExpressions;
using System.Threading;
using dIRca.Data;
using dIRca.Data.Settings;

namespace dIRca.Network
{
    /// <summary>
    /// Facade tying the Connection and Message services together and exposing events for the UI to consume parsed data
    /// </summary>
    public class ConnectionManager
    {
        private BackgroundWorker _workerLogin;
        private BackgroundWorker _workerListen;

        public bool IsListening { get; private set; }
        public bool IsLoggingIn { get; private set; }

        private IClient _settings;

        public bool IsAutoScrollOnOutput
        {
            get
            {
                if (_settings != null)
                    return _settings.AutoScrollOnOutput;
                else
                    return false;
            }
        }

        private bool _isConnected;
        public bool IsConnected
        {
            get { return _isConnected; }
            set 
            {
                if (_isConnected == value)
                    return;

                var oldValue = _isConnected;
                _isConnected = value;
            }
        }

        private ConnectionService _connectionService = null;
        private MessageService _messageService = null;
        private RecipientService _recipientService = null;
        private NetworkMonitor _networkMonitor = null;

        private Data.Settings.Network _network;
        private Server _server;
        private User _user;

        public string NetworkName { get; private set; }
        public string ServerName { get; private set; }
        public string NickName { get; private set; }

        public IMessagesView StatusLog
        {
            get
            {
                if (_recipientService != null)
                    return _recipientService.StatusLog;
                else
                    return null;
            }
        }

        public List<IMessagesView> Channels
        {
            get
            {
                if (_recipientService != null)
                    return _recipientService.Channels;
                else
                    return null;
            }
        }

        private bool _isRegistered;
        private bool _isOnline;

        private int _pingInterval = 60;
        private int _pingTimeout = 300;
        private DateTime _lastPingSent;
        private DateTime _lastPongReceived;
        private TimeSpan _lag;
        public TimeSpan Lag
        {
            get
            {
                if (_lastPingSent > _lastPongReceived)
                {
                    // there is an outstanding ping, thus we don't have a current lag value
                    return DateTime.Now - _lastPingSent;
                }

                return _lag;
            }
        }

        public LastConnection LastConnection { get; private set; }

        public event EventHandler<MessageEventArgs> MessageReceived;
        public event EventHandler<MessageEventArgs> MessageSent;

        public event EventHandler<RecipientEventArgs> ChannelAdded;
        public event EventHandler<RecipientEventArgs> ChannelRemoved;
        public event EventHandler<RefreshMessagesEventArgs> RefreshMessages;

        public ConnectionManager() : this(Factory.Create()) { }

        public ConnectionManager(IClient settings)
        {
            // TODO: Connection Throttle - move this out and choose the ILastConnection
            _settings = settings;
            _network = settings.Networks[0];
            _server = settings.Servers[0];
            _user = settings.User;

            _connectionService = new ConnectionService(_server, settings.DefaultPort);

            _messageService = new MessageService(_connectionService.Stream, _server.HostName, _user.NickName, _user.Email);
            _messageService.MessageReceived += new EventHandler<MessageEventArgs>(_messageService_MessageReceived);
            _messageService.MessageSent += new EventHandler<MessageEventArgs>(_messageService_MessageSent);

            _recipientService = new RecipientService();
            _recipientService.ChannelAdded += new EventHandler<RecipientEventArgs>(_recipientService_ChannelAdded);
            _recipientService.ChannelRemoved += new EventHandler<RecipientEventArgs>(_recipientService_ChannelRemoved);
            _recipientService.RefreshMessages += new EventHandler<RefreshMessagesEventArgs>(_recipientService_RefreshMessages);

            _networkMonitor = new NetworkMonitor();
            _networkMonitor.NetworkUp += new EventHandler(_networkMonitor_NetworkUp);
            _networkMonitor.NetworkDown += new EventHandler(_networkMonitor_NetworkDown);

            _connectionService.Connected += (s, e) =>
            {
                IsConnected = _connectionService.IsConnected;
                if (IsConnected)
                {
                    _messageService.Stream = _connectionService.Stream;
                    _recipientService.ServerName = _server.HostName;
                    Thread.Sleep(10);
                    if (_workerLogin.IsBusy && _workerLogin.WorkerSupportsCancellation && IsLoggingIn)
                        _workerLogin.CancelAsync();
                    IsListening = false;
                    IsLoggingIn = false;
                    Listen();
                }
            };

            _connectionService.Disconnected += (s, e) =>
            {
                IsConnected = _connectionService.IsConnected;
                if ((_workerLogin != null) && (_workerLogin.IsBusy && _workerLogin.WorkerSupportsCancellation && !IsLoggingIn))
                    _workerLogin.CancelAsync();
                if ((_workerListen != null) && (_workerListen.IsBusy && _workerListen.WorkerSupportsCancellation && !IsListening))
                    _workerListen.CancelAsync();
                Disconnect(_settings.ReconnectOnDisconnect);
            };

            ThreadPool.QueueUserWorkItem((item) =>
            {
                _networkMonitor.Start();
            });

            // Issue #13 Connect on Startup
            if (settings.ConnectOnStartup)
                ThreadPool.QueueUserWorkItem((item) =>
                {
                    Thread.Sleep(500);
                    Connect();
                });
        }

        void _networkMonitor_NetworkUp(object sender, EventArgs e)
        {
            // TODO: Need to signal to the UI that we can connect or not
        }

        void _networkMonitor_NetworkDown(object sender, EventArgs e)
        {
            Disconnect(false);
        }

        public void Connect()
        {
            if (!IsConnected)
            {
                _workerLogin = null;
                _workerLogin = new BackgroundWorker();
                _workerLogin.WorkerReportsProgress = true;
                _workerLogin.WorkerSupportsCancellation = true;
                _workerLogin.DoWork += (s, e) =>
                {
                    var now = DateTime.Now.AddMinutes(-1);
                    _lastPingSent = now;
                    _lastPongReceived = now;
                    IsLoggingIn = true;
                    Login(60, 99, 3);
                };
                _workerLogin.RunWorkerAsync();
            }
            // TODO: Cancel Login/Listen task if calling Connect again?
        }

        public void Disconnect(bool reconnect)
        {
            Disconnect();
            if (reconnect)
                Connect();
        }

        public void Disconnect()
        {
            if (IsConnected)
                Logout();
            // HACK: Setting this false here before they're actually set. Disconnect will roundtrip and *should* hit the event listener.
            IsLoggingIn = false;
            IsListening = false;
            _connectionService.Disconnect(true);
            OnMessageReceived(_messageService, new MessageEventArgs()
            {
                Message = new Message()
                {
                    ChannelName = "Status",
                    MessageData = " * Disconnected",
                },
            });
        }

        void Listen()
        {
            if (!IsListening)
            {
                _workerListen = null;
                _workerListen = new BackgroundWorker();
                _workerListen.WorkerReportsProgress = true;
                _workerListen.WorkerSupportsCancellation = true;
                _workerListen.DoWork += (s, e) =>
                {
                    IsListening = true;
                    // Should be infinite and cancel on Disconnect but just in case...
                    while (IsListening)
                    {
                        Thread.Sleep(10);
                        var message = _messageService.ReceiveMessage();
                    }
                };
                _workerListen.RunWorkerAsync();
            }
        }

        public void Login(int retryTimeout, int retryCount, int retryWait)
        {
            var isFirstLoop = true;
            var message = new Message();
            for (int index = 0; index < retryCount; index++)
            {
                if (IsConnected || !IsLoggingIn)
                    break;
                if (!isFirstLoop)
                {
                    Thread.Sleep(1000);
                    OnMessageReceived(_messageService, new MessageEventArgs()
                    {
                        Message = new Message()
                        {
                            ChannelName = "Status",
                            MessageData = " * Waiting " + retryWait + " seconds to retry",
                        },
                    });
                    Thread.Sleep(retryWait * 1000);
                    if (IsConnected)
                        continue;
                }
                if (isFirstLoop)
                {
                    isFirstLoop = false;
                    message = new Message()
                    {
                        ChannelName = "Status",
                        MessageData = " * Connecting to " + _server.HostName + " on port(s) " + _server.Ports,
                    };
                }
                else
                {
                    message = new Message()
                    {
                        ChannelName = "Status",
                        MessageData = " * Connecting to " + _server.HostName + " on port(s) " + _server.Ports + " retry: " + (index),
                    };
                }

                // TODO: Status "[timestamp] * Connecting to <server> on port <port>
                OnMessageReceived(_messageService, new MessageEventArgs()
                {
                    Message = message,
                });
                _connectionService.Connect();
                // Wait for Timeout
                // HACK: Tweak this timeout during testing of the login retry mechanism
                Thread.Sleep(retryTimeout * 1000);
                // HACK: Can't cancel the login process due to the Thread.Sleep call, so let's pretend shall we?
                if (!IsConnected && IsLoggingIn)
                {
                    OnMessageReceived(_messageService, new MessageEventArgs()
                    {
                        Message = new Message()
                        {
                            ChannelName = "Status",
                            MessageData = " * Unable to connect to server (Connection timed out)",
                        },
                    });
                }
            }
        }

        void SendCredentials()
        {
            // TODO: Find a way to tap into the event stream to send prompts
            // ReceiveType = Error/ErrorMessage
             if (!string.IsNullOrEmpty(_server.Password))
                 _messageService.SendMessage("PASS " + _server.Password);
            // ReplyCode = ErrorNeedMoreParams
            // ReplyCode = ErrorAlreadyRegistered
            // TODO: NickName Alternates time and prompt if you run out of juice... This changes using the normal nick command
             Thread.Sleep(500);
             _messageService.SendMessage("NICK :" + _user.NickName);
            // ReplyCode = ErrorNoNicknameGiven
            // ReplyCode = ErrorErroneusNickname
            // ReplyCode = ErrorNicknameInUse
            // ReplyCode = ErrorNicknameCollision
            // ReplyCode = ErrorUnavailableResource
            // ReplyCode = ErrorRestricted
             Thread.Sleep(500);
            // mIRC uses 0, or my script's settings equate to it this. I blindly follow the leader
             _messageService.SendMessage("USER " + _user.NickName + " 0 * :" + _user.Name);
            // ReplyCode = ErrorNeedMoreParams
            // ReplyCode = ErrorAlreadyRegistered
        }

        void UpdateServerEntry(string serverName)
        {
            _recipientService.ServerName = serverName;
            LastConnection = new LastConnection()
            {
                Network = _network,
                Server = _server,
            };
            LastConnection.Server.HostName = serverName;
        }

        void Logout()
        {
            _messageService.SendMessage("QUIT " + _user.QuitMessage);
        }

        [Obsolete]
        public void SendMessage(string message)
        {
            if (_messageService != null)
                _messageService.SendMessage(message);
        }

        public void SendMessage(string destination, string message)
        {
            if (_messageService != null)
                if (destination.ToLower() == "status")
                    _messageService.SendMessage(message);
                else
                    _messageService.SendMessage(SendType.Message, destination, message);
        }

        void _messageService_MessageReceived(object sender, MessageEventArgs e)
        {
            OnMessageReceived(sender, e);
            SimpleParser(e.Message);
        }

        void _messageService_MessageSent(object sender, MessageEventArgs e)
        {
            OnMessageSent(sender, e);
        }

        void _recipientService_ChannelAdded(object sender, RecipientEventArgs e)
        {
            OnChannelAdded(e);
        }

        void _recipientService_ChannelRemoved(object sender, RecipientEventArgs e)
        {
            OnChannelRemoved(e);
        }

        void _recipientService_RefreshMessages(object sender, RefreshMessagesEventArgs e)
        {
            OnRefreshMessages(e);
        }

        void OnMessageReceived(object sender, MessageEventArgs e)
        {
            _recipientService.Add(e.Message);
            var handler = MessageReceived;
            if (handler != null)
                handler(sender, e);
        }

        void OnMessageSent(object sender, MessageEventArgs e)
        {
            _recipientService.Add(e.Message);
            var handler = MessageSent;
            if (handler != null)
                handler(sender, e);
        }

        void OnChannelAdded(RecipientEventArgs e)
        {
            var handler = ChannelAdded;
            if (handler != null)
                handler(this, e);
        }

        void OnChannelRemoved(RecipientEventArgs e)
        {
            var handler = ChannelRemoved;
            if (handler != null)
                handler(this, e);
        }

        void OnRefreshMessages(RefreshMessagesEventArgs e)
        {
            var handler = RefreshMessages;
            if (handler != null)
                handler(this, e);
        }

        private void SimpleParser(Message message)
        {
            string rawline = message.RawMessageData;
            string[] rawlineex = rawline.Split(new char[] { ' ' });

            // Parse Welcome messages
            if (message.ReplyCode != ReplyCode.Null)
            {
                switch (message.ReplyCode)
                {
                    case ReplyCode.Welcome:
                        SetNetworkAndNickName(message.MessageData);
                        _isRegistered = true;
                        break;
                    case ReplyCode.YourHost:
                        ServerName = GetServerName(message.MessageData);
                        UpdateServerEntry(ServerName);
                        break;
                    case ReplyCode.EndOfMotd:
                        ProcessNetworkCommands();
                        break;
                    case ReplyCode.ErrorNoMotd:
                        ProcessNetworkCommands();
                        break;
                }
            }
            else
            {
                // Send Login
                if (message.Count == 1)
                    SendCredentials();
                switch (rawlineex[0])
                {
                    case "PING":
                        PingResponse(rawlineex[1]);
                        break;
                }
                switch (message.ReceiveType)
                {
                    case ReceiveType.NickChange:
                        // HACK: Process other user nick changes in channels. Remove the old user, add the new. 
                        if (NickName == message.NickName)
                            SetNickName(message.MessageData);
                        break;
                }
            }
        }

        private void SetNetworkAndNickName(string message)
        {
            // EFNet: Welcome to the $network Internet Relay Chat Network $nick
            // PowerPrecision: Welcome to the $network IRC Network $nick!$email@$host
            var welcomeRegex = new Regex("^.*?Welcome to the (.*?) (IRC|Internet Relay Chat) Network (.*)", RegexOptions.Compiled);
            var found = welcomeRegex.Match(message);
            NetworkName = found.Groups[1].Value;

            var myIdent = found.Groups[3].Value;
            if (myIdent.IndexOf('!') > -1)
                SetNickName(myIdent.Substring(0, myIdent.IndexOf('!')));
            else
                SetNickName(myIdent);
        }

        private void SetNickName(string nickName)
        {
            NickName = nickName;
            _recipientService.NickName = NickName;
            _messageService.NickName = NickName;
        }

        private string GetServerName(string message)
        {
            string result = null;

            // Your host is $server, running version $version
            var hostRegex = new Regex("^.*?Your host is (.*),", RegexOptions.Compiled);
            var found = hostRegex.Match(message);
            result = found.Groups[1].Value;

            // Issue #23 EFNet server name parse includes [ip/port]. This strips that out
            if (result.IndexOf('[') > -1)
                result = result.Substring(0, result.IndexOf('['));
            return result;
        }

        private void PingResponse(string pongMessage)
        {
            var now = DateTime.Now;
            int last_ping_sent = (int)(now - _lastPingSent).TotalSeconds;
            int last_pong_rcvd = (int)(now - _lastPongReceived).TotalSeconds;
            // determines if the resoponse time is ok
            if (last_ping_sent < _pingTimeout)
            {
                if (_lastPingSent > _lastPongReceived)
                {
                    // there is a pending ping request, we have to wait
                    return;
                }

                // determines if it need to send another ping yet
                if (last_pong_rcvd > _pingInterval)
                {
                    _messageService.SendMessage("PONG " + pongMessage);
                    _lastPingSent = now;
                    _lastPongReceived = now;
                    _lag = now - _lastPingSent;
                } // else connection is fine, just continue
            }
            //else
            //{
            //    if (_Connection.IsDisconnecting)
            //    {
            //        break;
            //    }
            //    // only flag this as connection error if we are not
            //    // cleanly disconnecting
            //    _Connection.IsConnectionError = true;
            //}
        }

        private void ProcessNetworkCommands()
        {
            if (!_isOnline)
            {
                _isOnline = true;
                if (NetworkName.ToLower() == _network.Name.ToLower())
                {
                    if (_network.IsJoinEnabled)
                    {
                        var delay = _network.JoinDelay;
                        var channels = _network.JoinChannels.Split(new char[] { ',' }).ToList();
                        channels.ForEach(item =>
                        {
                            var timer = new Timer(new TimerCallback((objectState) =>
                            {
                                _messageService.SendMessage("JOIN " + objectState as string);
                            }), item.ToUpper(), delay + 1000, Timeout.Infinite); // update the network type every 3 seconds.
                            delay += delay;
                            Thread.Sleep(1000);
                        });
                    }
                }
            }
        }
    }
}
